package net.zoneland.knowledge.aop;

import java.lang.reflect.Method;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import net.zoneland.knowledge.constant.Constants;
import net.zoneland.knowledge.model.LoginUserPojo;
import net.zoneland.knowledge.model.security.OperationContextHolder;
import net.zoneland.knowledge.utils.GsonUtils;
import net.zoneland.knowledge.utils.LoginUtils;
import net.zoneland.knowledge.utils.OplogUtils;

/**
 * controller对应方法aop监控.
 * @author zmq
 * 2021/10/21 13:54
 */
@Aspect
@Component
@Lazy(false)
public class ControllerMethodRecordAspect {
    /**
     * 日志.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(ControllerMethodRecordAspect.class);

    /**
     * 是否开启性能监控.
     */
    @Value("${stop.watch.controller.enabled:false}")
    private boolean stopWatchEnabled;
    /**
     * 字符串长度.
     */
    private static final int MAX_LENGTH = 1000;

    /**
     * 切点.
     */
    @Pointcut("execution(* net.zoneland.knowledge.controller.*Controller.*(..))")
    public void controllerCut() {

    }

    /**
     * 环绕通知.
     * @param joinPoint 方法执行体
     * @return 返回结果
     * @throws Exception 异常
     */
    @SuppressWarnings("checkstyle:IllegalThrows")
    @Around("controllerCut()")
    public Object around(final ProceedingJoinPoint joinPoint) throws Throwable {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = null;
        if (ra != null) {
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            request = sra.getRequest();
        }
        return aroundEvent(joinPoint, stopWatchEnabled, request);
    }


    /**
     * 环绕通知.
     * @param joinPoint 方法执行体
     * @param stopWatchEnabled 是否开启监控
     * @param request http请求
     * @return 返回结果
     * @throws Throwable 异常
     */
    @SuppressWarnings("checkstyle:IllegalThrows")
    public static Object aroundEvent(final ProceedingJoinPoint joinPoint,
                                     final boolean stopWatchEnabled,
                                     final HttpServletRequest request) throws Throwable {
        Object object = null;
        StopWatch sw = null;
        Throwable exception = null;
        try {
            if (stopWatchEnabled) {
                sw = StopWatch.createStarted();
                // 执行源方法
                object = joinPoint.proceed();
            } else {
                // 执行源方法
                object = joinPoint.proceed();
            }
        } catch (Throwable e) {
            LOGGER.error("controller方法执行业务执行异常,requestURI:{}.", request == null ? "" : request.getRequestURI(), e);
            exception = e;
        } finally {
            if (stopWatchEnabled) {
                long splitTime = -1L;
                if (sw != null) {
                    sw.split();
                    splitTime = sw.getSplitTime();
                }
                printOpLog(joinPoint, object, splitTime, request, exception);
                if (sw != null) {
                    sw.stop();
                }
            }
        }
        if (exception != null) {
            throw exception;
        }
        return object;
    }

    /**
     * 日志输出.
     * @param joinPoint 切点
     * @param returnObj 返回对象
     * @param consumerTime 耗时
     * @param request http请求
     * @param exception 异常
     */
    private static void printOpLog(final ProceedingJoinPoint joinPoint, final Object returnObj,
                                 final long consumerTime, final HttpServletRequest request,
                                   final Throwable exception) {
        try {
            if (request != null) {
                OplogUtils.OpLogBean opLog = new OplogUtils.OpLogBean();
                Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
                opLog.setConsumerTime(consumerTime)
                    .setExceptionMsg(exception != null ? exception.getMessage() : "")
                    .setMethod(method.getDeclaringClass().getName() + "." + method.getName())
                    .setQueryStr(request.getQueryString())
                    .setRequestArgs(postArgs(joinPoint))
                    .setReturnJson(serialJsonStr(returnObj))
                    .setToken(request.getHeader(Constants.TOKEN_NAME))
                    .setRequestUri(request.getRequestURI())
                    .setUserId(getCurUser(request));
                OplogUtils.sendOpLog(request, opLog);
            }
        } catch (Exception e) {
            LOGGER.error("日志输出失败，method:{}", joinPoint.getSignature().toString(), e);
        }
    }

    /**
     * 获取当前用户id.
     * @param request 当前用户uid
     * @return 用户uid
     */
    private static String getCurUser(final HttpServletRequest request) {
        final String token = request.getHeader(Constants.TOKEN_NAME);
        String userId = request.getRemoteUser();
        if (StringUtils.isNotBlank(token)) {
            LoginUserPojo curUser = LoginUtils.getCurrentUserByToken(token);
            userId = curUser == null ? "" : curUser.getUserID();
        } else if (OperationContextHolder.getPrincipal() != null) {
            userId = OperationContextHolder.getPrincipal().getUserId();
        }
        return userId;
    }
    /**
     * 日志输出.
     * @param joinPoint 切点
     * @param returnObj 返回对象
     * @param consumerTime 耗时
     * @param request http请求
     */
    private static void printLog(final ProceedingJoinPoint joinPoint, final Object returnObj,
                                 final long consumerTime, final HttpServletRequest request) {
        try {
            LOGGER.info("==controller method watch, requestURI:[{}], token:[{}], args:[{}], return:[{}], consumerTime:[{}]",
                Optional.ofNullable(request).map(it -> it.getRequestURI()).orElse(""),
                Optional.ofNullable(request).map(it -> it.getHeader(Constants.TOKEN_NAME)).orElse(""),
                Optional.ofNullable(joinPoint).map(it -> postArgs(joinPoint)).orElse(""),
                serialJsonStr(returnObj),
                consumerTime);
        } catch (Exception e) {
            LOGGER.error("日志输出失败，method:{}", joinPoint.getSignature().toString(), e);
        }
    }

    /**
     * 获取post传参.
     * @param joinPoint 执行对象
     * @return 参数
     */
    private static String postArgs(final ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String[] parameterNames = methodSignature.getParameterNames();
        Class[] parameterTypes = methodSignature.getParameterTypes();
        StringBuilder sb = new StringBuilder("");
        if (parameterTypes.length > 0) {
            Object[] args = joinPoint.getArgs();
            for (int index = 0; index < parameterTypes.length; index++) {
                Class parameterType = parameterTypes[index];
                if (StringUtils.equals(parameterType.getName(), HttpServletRequest.class.getName())
                    || StringUtils.equals(parameterType.getName(), HttpServletResponse.class.getName())) {
                    continue;
                }
                sb.append(parameterNames[index]).append(":")
                    .append(args[index] == null ? "NULL" : args[index].toString())
                    .append(",");
            }
        }
        return sb.toString();
    }

    /**
     * 转换返回结果.
     * @param object 返回结果
     * @return 字符串
     */
    private static String serialJsonStr(final Object object) {
        String res = "";
        try {
            res = Optional.ofNullable(object).map(it -> GsonUtils.toJson(object)).orElse("");
        } catch (Exception e) {
            LOGGER.error("json序列化异常", e);
            res = object.toString();
        }
        if (StringUtils.length(res) > MAX_LENGTH) {
            res = res.substring(0, MAX_LENGTH) + "...";
        }
        return res;
    }
}
